"""
保温杯中性策略2期 | 邢不行 | 2023分享会
author: 邢不行
微信: xbx6660
"""
import math
import time
import traceback
import pandas as pd
from datetime import datetime
from utils.commons import retry_wrapper
from config import utc_offset,root_path


# =====获取数据
# 获取单个币种的1天数据
def fetch_all_binance_swap_candle_data(exchange, symbol_list, run_time, limit, interval='1h', fundrate_use_path_=None):
    # # ========================= 资金费率  ========================== up
    fundrate_use_path_ = root_path+r'/data/fundrate_use.csv'
    premiumindex_df = pd.read_csv(fundrate_use_path_, encoding='gbk', parse_dates=['candle_begin_time'])
    premiumindex_df = premiumindex_df[['candle_begin_time', 'symbol', 'fundingRate']]
    # # ========================= 资金费率  === =====================down

    # 这里用dict存储，方便后面数据处理操作
    result = {}
    for symbol in symbol_list:
        # 获取k线数据
        df = fetch_binance_swap_candle_data(exchange, symbol, run_time, limit, interval=interval)

        # 返回None或者空的df，不放到result里
        if df is None or df.empty:
            continue
        # =================================================== up
        else:
            df = pd.merge(left=df, right=premiumindex_df, on=['symbol', 'candle_begin_time'], how='left', sort=True,
                          indicator=True)
            if pd.isna(df.iloc[-12]['fundingRate']):  # 如果你现在保存的历史数据还不足12小时（时间取决于你自己），那么向后填充
                df['fundingRate'] = df['fundingRate'].fillna(method='bfill', axis=0)
            else:  # 如果你现在保存的历史数据足够12小时（时间取决于你自己），那么向前填充
                df['fundingRate'] = df['fundingRate'].fillna(method='ffill', axis=0)
            del df['_merge']
        # =================================================== down

        # 将数据添加到result中
        result[symbol] = df

    return result


def fetch_binance_swap_candle_data(exchange, symbol, run_time, limit=1500, interval='1h'):
    # ===获取K线数据
    # 计算获取k线的开始时间
    start_time_dt = run_time - pd.to_timedelta(interval) * limit
    params = {
        'symbol': symbol,  # 获取币种
        'interval': interval,  # 获取k线周期
        'limit': limit,  # 获取多少根
        'startTime': int(time.mktime(start_time_dt.timetuple())) * 1000  # 获取币种开始时间
    }
    # 获取指定币种的k线数据
    try:
        kline = retry_wrapper(exchange.fapiPublic_get_klines, params=params, func_name='获取币种K线', if_exit=False)
    except Exception as e:
        print(traceback.format_exc())
        # 如果获取k线重试出错，直接返回，当前币种不参与交易
        return pd.DataFrame()

    # ===整理数据
    # 将数据转换为DataFrame
    df = pd.DataFrame(kline, dtype='float')
    # 对字段进行重命名，字段对应数据可以查询文档（https://binance-docs.github.io/apidocs/futures/cn/#k）
    columns = {0: 'candle_begin_time', 1: 'open', 2: 'high', 3: 'low', 4: 'close', 5: 'volume', 6: 'close_time',
               7: 'quote_volume',
               8: 'trade_num', 9: 'taker_buy_base_asset_volume', 10: 'taker_buy_quote_asset_volume', 11: 'ignore'}
    df.rename(columns=columns, inplace=True)

    df['symbol'] = symbol  # 添加symbol列
    df.sort_values(by=['candle_begin_time'], inplace=True)  # 排序
    df.drop_duplicates(subset=['candle_begin_time'], keep='last', inplace=True)  # 去重

    # 转换数据时区
    df['candle_begin_time'] = pd.to_datetime(df['candle_begin_time'], unit='ms') + pd.Timedelta(hours=utc_offset)

    # 删除runtime那根未走完的k线数据（交易所有时候会返回这条数据）
    df = df[df['candle_begin_time'] < run_time]
    df.reset_index(drop=True, inplace=True)

    return df


# 获取所有币种永续合约数据的1小时K线数据
# def fetch_all_binance_swap_candle_data(exchange, symbol_list, run_time, limit, interval='1h'):
#     """
#     获取所有币种永续合约数据的1天K线数据
#
#     :param exchange:    交易所对象，用于获取数据
#     :param symbol_list: 币种泪飙
#     :param run_time:    当前运行时间
#     :param limit:       请求k线数量
#     :param interval:    获取k线的周期
#     :return:
#
#     {
#     'BTCUSDT':
#                             symbol  ... taker_buy_quote_asset_volume
#             0     BTCUSDT  ...                 1.451404e+08
#             1     BTCUSDT  ...                 1.492456e+08
#             2     BTCUSDT  ...                 1.200780e+08
#             3     BTCUSDT  ...                 9.680288e+07
#             4     BTCUSDT  ...                 6.867702e+08
#             ...       ...  ...                          ...
#             1495  BTCUSDT  ...                 1.858995e+08
#             1496  BTCUSDT  ...                 1.151737e+08
#             1497  BTCUSDT  ...                 8.091855e+07
#             1498  BTCUSDT  ...                 1.037028e+08
#             1499  BTCUSDT  ...                 1.111743e+07,
#
#     'ETHUSDT':
#             symbol  ... taker_buy_quote_asset_volume
#             0     ETHUSDT  ...                 2.023519e+08
#             1     ETHUSDT  ...                 1.813869e+08
#             2     ETHUSDT  ...                 1.298206e+08
#             3     ETHUSDT  ...                 1.544976e+08
#             4     ETHUSDT  ...                 6.494550e+08
#             ...       ...  ...                          ...
#             1495  ETHUSDT  ...                 2.792866e+08
#             1496  ETHUSDT  ...                 1.220917e+08
#             1497  ETHUSDT  ...                 7.935349e+07
#             1498  ETHUSDT  ...                 1.557781e+08
#             1499  ETHUSDT  ...                 2.241793e+07,
#     ......
#     }
#     """
#     # 这里用dict存储，方便后面数据处理操作
#     result = {}
#     for symbol in symbol_list:
#         # 获取k线数据
#         df = fetch_binance_swap_candle_data(exchange, symbol, run_time, limit, interval=interval)
#         # 返回None或者空的df，不放到result里
#         if df is None or df.empty:
#             continue
#         # 将数据添加到result中
#         result[symbol] = df
#
#     return result


# 获取币安的ticker数据
def fetch_binance_ticker_data(exchange):
    # 获取所有币种的ticker数据
    tickers = retry_wrapper(exchange.fapiPublic_get_ticker_price, func_name='获取所有币种的ticker数据')
    tickers = pd.DataFrame(tickers)
    tickers.set_index('symbol', inplace=True)
    return tickers['price']

def load_market(exchange, black_list):
    """
    加载市场数据

    :param exchange:    交易所对象，用于获取数据
    :param black_list:  黑名单，不参与交易的币种
    :return:

    min_qty         最小下单精度    例： 3 代表 0.001
        {'BTCUSDT': 3, 'ETHUSDT': 3, 'BCHUSDT': ,....}3
    price_precision 币种价格精     例： 2 代表 0.01
        {'BTCUSDT': 1, 'ETHUSDT': 2, 'BCHUSDT': 2, 'XRPUSDT': 4,...}
    min_notional    最小下单金额    例： 5.0 代表 最小下单金额是5U
        {'BTCUSDT': 5.0, 'ETHUSDT': 5.0, 'BCHUSDT': 5.0, 'XRPUSDT': 5.0...}
    """
    # ===获取交易对的信息
    exchange_info = retry_wrapper(exchange.fapiPublic_get_exchangeinfo, func_name='获取交易对的信息')

    # ===挑选出所有符合条件的交易对
    now_ms = datetime.now().timestamp() * 1000
    symbol_dict_list = list(filter(lambda s: ((now_ms - int(s['onboardDate'])) / 1000 / 86400) >= 7  # 挑选上架时间超过7天的交易对
                                             and s['status'] == 'TRADING'  # 挑选交易状态的交易对
                                             and s['quoteAsset'] == 'USDT'  # 挑选U本位结算的交易对
                                             and s['contractType'] == 'PERPETUAL', exchange_info['symbols']))
    symbol_list = [x['symbol'] for x in symbol_dict_list]  # 获取所有的交易对名称

    # 删除在黑名单中的币种
    symbol_list = set(symbol_list) - set(black_list)  # 使用set集合操作，去除黑名单
    symbol_list = list(symbol_list)  # 将set转成list，方便后续操作

    # ===获取各个交易对的精度、下单量等信息
    min_qty = {}  # 最小下单精度，例如bnb，一次最少买入0.001个
    price_precision = {}  # 币种价格精，例如bnb，价格是158.887，不能是158.8869
    min_notional = {}  # 最小下单金额，例如bnb，一次下单至少买入金额是5usdt
    # 遍历获得想要的数据
    for info in exchange_info['symbols']:
        symbol = info['symbol']
        for _filter in info['filters']:
            if _filter['filterType'] == 'PRICE_FILTER':
                price_precision[symbol] = int(math.log(float(_filter['tickSize']), 0.1))
            elif _filter['filterType'] == 'LOT_SIZE':
                min_qty[symbol] = int(math.log(float(_filter['minQty']), 0.1))
            elif _filter['filterType'] == 'MIN_NOTIONAL':
                min_notional[symbol] = float(_filter['notional'])

    return symbol_list, min_qty, price_precision, min_notional


# 重置一下页面最大杠杆
def reset_leverage(exchange, max_leverage=3, coin_list=[]):
    """
    重置一下页面最大杠杆
    :param exchange:        交易所对象，用于获取数据
    :param max_leverage:    设置页面最大杠杆
    :param coin_list:       对指定币种进行调整页面杠杆
    """
    # 获取账户持仓风险（这里有杠杆数据）
    position_risk = retry_wrapper(exchange.fapiPrivateV2_get_positionrisk, func_name='获取账户持仓风险')
    position_risk = pd.DataFrame(position_risk)  # 将数据转成DataFrame
    if len(coin_list) > 0:
        position_risk = position_risk[position_risk['symbol'].isin(coin_list)]  # 只对选币池中的币种进行调整页面杠杆
    position_risk.set_index('symbol', inplace=True)  # 将symbol设为index

    # 遍历每一个可以持仓的币种，修改页面最大杠杆
    for symbol, row in position_risk.iterrows():
        if int(row['leverage']) != max_leverage:
            # 设置杠杆
            retry_wrapper(exchange.fapiPrivate_post_leverage,
                          params={'symbol': symbol, 'leverage': max_leverage},
                          func_name='设置杠杆')

    print('修改页面最大杠杆操作完成')


# 获取最近n天的资金费信息
def fetch_binance_funding_fee(exchange, run_time, during_days='30D'):
    """
    获取最近n天的资金费信息
    :param exchange:        交易所对象，用于获取数据
    :param run_time:        当前时间
    :param during_days:     获取最近多少天的数据
    """
    start_time = run_time - pd.Timedelta(during_days)
    param = {
        'limit': 1000,
        'startTime': int(time.mktime(start_time.timetuple())) * 1000,
        'incomeType': 'FUNDING_FEE',  # 资金费
    }
    df_list = []
    while True:
        try:
            # 获取资金流水
            result = retry_wrapper(exchange.fapiPrivate_get_income, params=param, func_name='获取账户资金流水',
                                   if_exit=False)
        except Exception as e:
            print(traceback.format_exc())
            break  # 跳出循环

        df = pd.DataFrame(result)
        if df.empty:
            break
        # 时间转换
        df['time'] = pd.to_datetime(df['time'], unit='ms') + pd.Timedelta(hours=utc_offset)
        df['income'] = df['income'].astype(float)
        df_list.append(df)

        if df.iloc[-1]['time'] >= run_time:
            break
        else:
            param['startTime'] = int(time.mktime(df.iloc[-1]['time'].timetuple())) * 1000

    # 空数据，直接返回空df
    if not df_list:
        return pd.DataFrame()

    # 数据合并
    all_income_df = pd.concat(df_list, ignore_index=True)
    all_income_df.sort_values('time', inplace=True)
    all_income_df.drop_duplicates(subset=['time', 'symbol'], inplace=True)
    all_income_df.reset_index(inplace=True, drop=True)

    return all_income_df


# 检查是否开启了联合保证金模式
def check_multi_assets_margin(exchange):
    """
    检查是否开启了联合保证金模式

    :param exchange:        交易所对象，用于获取数据
    """
    # 查询保证金模式
    res = retry_wrapper(exchange.fapiPrivate_get_multiassetsmargin, func_name='fapiPrivate_get_multiassetsmargin',
                        if_exit=False)
    # 判断是否开启了联合保证金模式
    if not bool(res['multiAssetsMargin']):  # 若联合保证金模式没有开启，则调用接口开启一下联合保证金模式
        params = {'multiAssetsMargin': 'true'}
        retry_wrapper(exchange.fapiPrivate_post_multiassetsmargin, params=params,
                      func_name='fapiPrivate_post_multiassetsmargin', if_exit=False)
        print('开启联合保证金模式')


# 检查是否是单向持仓模式
def check_position_side(exchange):
    """
    检查是否是单向持仓模式

    :param exchange:        交易所对象，用于获取数据
    """
    # 查询持仓模式
    res = retry_wrapper(exchange.fapiPrivate_get_positionside_dual, func_name='fapiPrivate_get_positionside_dual',
                        if_exit=False)
    # 判断是否是单向持仓模式
    if bool(res['dualSidePosition']):  # 若当前持仓模式不是单向持仓模式，则调用接口修改持仓模式为单向持仓模式
        params = {'dualSidePosition': 'false'}
        retry_wrapper(exchange.fapiPrivate_post_positionside_dual, params=params,
                      func_name='fapiPrivate_post_positionside_dual', if_exit=False)
        print('修改持仓模式为单向持仓')
